/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.usergrid.mongo.testproxy;


import java.net.InetSocketAddress;
import java.nio.ByteOrder;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.buffer.HeapChannelBufferFactory;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.ClientSocketChannelFactory;
import org.apache.usergrid.mongo.MongoMessageDecoder;
import org.apache.usergrid.mongo.protocol.Message;


public class MongoProxyInboundHandler extends SimpleChannelUpstreamHandler {

    private final ClientSocketChannelFactory cf;
    private final String remoteHost;
    private final int remotePort;

    // This lock guards against the race condition that overrides the
    // OP_READ flag incorrectly.
    // See the related discussion: http://markmail.org/message/x7jc6mqx6ripynqf
    final Object trafficLock = new Object();

    private volatile Channel outboundChannel;


    public MongoProxyInboundHandler( ClientSocketChannelFactory cf, String remoteHost, int remotePort ) {
        this.cf = cf;
        this.remoteHost = remoteHost;
        this.remotePort = remotePort;
    }


    @Override
    public void channelOpen( ChannelHandlerContext ctx, ChannelStateEvent e ) throws Exception {
        // Suspend incoming traffic until connected to the remote host.
        final Channel inboundChannel = e.getChannel();
        inboundChannel.setReadable( false );

        // Start the connection attempt.
        ClientBootstrap cb = new ClientBootstrap( cf );
        cb.setOption( "bufferFactory", HeapChannelBufferFactory.getInstance( ByteOrder.LITTLE_ENDIAN ) );
        cb.getPipeline().addLast( "framer", new MongoMessageFrame() );
        cb.getPipeline().addLast( "handler", new OutboundHandler( e.getChannel() ) );
        ChannelFuture f = cb.connect( new InetSocketAddress( remoteHost, remotePort ) );

        outboundChannel = f.getChannel();
        f.addListener( new ChannelFutureListener() {
            @Override
            public void operationComplete( ChannelFuture future ) throws Exception {
                if ( future.isSuccess() ) {
                    // Connection attempt succeeded:
                    // Begin to accept incoming traffic.
                    inboundChannel.setReadable( true );
                }
                else {
                    // Close the connection if the connection attempt has
                    // failed.
                    inboundChannel.close();
                }
            }
        } );
    }


    @Override
    public void messageReceived( ChannelHandlerContext ctx, final MessageEvent e ) throws Exception {
        ChannelBuffer msg = ( ChannelBuffer ) e.getMessage();
        Message mongo_message = MongoMessageDecoder.decode( msg );
        if ( mongo_message != null ) {
            System.out.println( ">>> " + mongo_message.toString() );
        }
        synchronized ( trafficLock ) {
            outboundChannel.write( msg );
            // If outboundChannel is saturated, do not read until notified in
            // OutboundHandler.channelInterestChanged().
            if ( !outboundChannel.isWritable() ) {
                e.getChannel().setReadable( false );
            }
        }
    }


    @Override
    public void channelInterestChanged( ChannelHandlerContext ctx, ChannelStateEvent e ) throws Exception {
        // If inboundChannel is not saturated anymore, continue accepting
        // the incoming traffic from the outboundChannel.
        synchronized ( trafficLock ) {
            if ( e.getChannel().isWritable() ) {
                outboundChannel.setReadable( true );
            }
        }
    }


    @Override
    public void channelClosed( ChannelHandlerContext ctx, ChannelStateEvent e ) throws Exception {
        if ( outboundChannel != null ) {
            closeOnFlush( outboundChannel );
        }
    }


    @Override
    public void exceptionCaught( ChannelHandlerContext ctx, ExceptionEvent e ) throws Exception {
        e.getCause().printStackTrace();
        closeOnFlush( e.getChannel() );
    }


    private class OutboundHandler extends SimpleChannelUpstreamHandler {

        private final Channel inboundChannel;


        OutboundHandler( Channel inboundChannel ) {
            this.inboundChannel = inboundChannel;
        }


        @Override
        public void messageReceived( ChannelHandlerContext ctx, final MessageEvent e ) throws Exception {
            ChannelBuffer msg = ( ChannelBuffer ) e.getMessage();
            Message mongo_message = MongoMessageDecoder.decode( msg );
            if ( mongo_message != null ) {
                System.out.println( "<<< " + mongo_message.toString() + "\n" );
            }
            synchronized ( trafficLock ) {
                inboundChannel.write( msg );
                // If inboundChannel is saturated, do not read until notified in
                // HexDumpProxyInboundHandler.channelInterestChanged().
                if ( !inboundChannel.isWritable() ) {
                    e.getChannel().setReadable( false );
                }
            }
        }


        @Override
        public void channelInterestChanged( ChannelHandlerContext ctx, ChannelStateEvent e ) throws Exception {
            // If outboundChannel is not saturated anymore, continue accepting
            // the incoming traffic from the inboundChannel.
            synchronized ( trafficLock ) {
                if ( e.getChannel().isWritable() ) {
                    inboundChannel.setReadable( true );
                }
            }
        }


        @Override
        public void channelClosed( ChannelHandlerContext ctx, ChannelStateEvent e ) throws Exception {
            closeOnFlush( inboundChannel );
        }


        @Override
        public void exceptionCaught( ChannelHandlerContext ctx, ExceptionEvent e ) throws Exception {
            e.getCause().printStackTrace();
            closeOnFlush( e.getChannel() );
        }
    }


    /** Closes the specified channel after all queued write requests are flushed. */
    static void closeOnFlush( Channel ch ) {
        if ( ch.isConnected() ) {
            ch.write( ChannelBuffers.EMPTY_BUFFER ).addListener( ChannelFutureListener.CLOSE );
        }
    }
}
